package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/schollz/progressbar/v3" // Progress bar library
	"gopkg.in/ini.v1"
)

type Submodule struct {
	Name string
	Path string
	URL  string
}

func main() {
	// Custom usage message
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] [GITMODULES_FILE]\n", os.Args[0])
		fmt.Fprintln(os.Stderr, "Clones or updates submodules from a .gitmodules file with proxy support")
		fmt.Fprintln(os.Stderr, "\nOptions:")
		flag.PrintDefaults()
		fmt.Fprintln(os.Stderr, "\nArguments:")
		fmt.Fprintln(os.Stderr, "  GITMODULES_FILE   Path to .gitmodules file (default: .gitmodules)")
	}

	// Parse command line flags
	proxy := flag.String("proxy", "https://ghproxy.cn/", "URL prefix for GitHub proxy (e.g., https://ghproxy.cn/)")
	concurrency := flag.Int("concurrency", 5, "Maximum concurrent operations")
	flag.Parse()

	// Determine .gitmodules file path
	gitmodulesPath := ".gitmodules"
	if len(flag.Args()) > 0 {
		gitmodulesPath = flag.Args()[0]
	}

	// Validate file exists
	if _, err := os.Stat(gitmodulesPath); os.IsNotExist(err) {
		log.Fatalf("Error: .gitmodules file not found at %q", gitmodulesPath)
	}

	// Load and parse .gitmodules
	cfg, err := ini.Load(gitmodulesPath)
	if err != nil {
		log.Fatalf("Failed to load %s: %v", gitmodulesPath, err)
	}

	// Collect submodules
	var submodules []Submodule
	for _, section := range cfg.Sections() {
		if section.Name() == "DEFAULT" {
			continue
		}
		if !section.HasKey("path") || !section.HasKey("url") {
			log.Printf("Skipping invalid section: %s", section.Name())
			continue
		}

		submodules = append(submodules, Submodule{
			Name: section.Name()[len("submodule \"") : len(section.Name())-1],
			Path: section.Key("path").String(),
			URL:  section.Key("url").String(),
		})
	}

	if len(submodules) == 0 {
		log.Printf("No submodules found in %s", gitmodulesPath)
		return
	}

	// Initialize progress bar (total = number of submodules)
	bar := progressbar.NewOptions(len(submodules),
		progressbar.OptionSetDescription("Processing submodules..."),
		progressbar.OptionSetWidth(40),
		progressbar.OptionShowCount(),
		progressbar.OptionThrottle(100*time.Millisecond),
		progressbar.OptionShowElapsedTimeOnFinish(),
	)
	var barMutex sync.Mutex // For thread-safe progress bar updates

	// Concurrency control
	semaphore := make(chan struct{}, *concurrency)
	var wg sync.WaitGroup
	errChan := make(chan error, len(submodules))

	// Process submodules concurrently
	for _, sm := range submodules {
		wg.Add(1)
		go func(s Submodule) {
			defer wg.Done()
			semaphore <- struct{}{}
			defer func() { <-semaphore }()

			// Get URL (proxied only if it's a GitHub repo)
			targetURL := getProxiedURL(s.URL, *proxy)
			log.Printf("Processing %s -> %s (from %s)", s.Name, s.Path, targetURL)

			// Check path and handle clone/update
			pathInfo, err := os.Stat(s.Path)
			switch {
			case os.IsNotExist(err):
				if err := cloneSubmodule(s.Path, targetURL); err != nil {
					errChan <- fmt.Errorf("failed to clone %s: %v", s.Name, err)
				} else {
					log.Printf("Successfully cloned %s", s.Name)
				}

			case err != nil:
				errChan <- fmt.Errorf("failed to check path for %s: %v", s.Name, err)

			case pathInfo.IsDir():
				if err := updateSubmodule(s.Path, targetURL); err != nil {
					errChan <- fmt.Errorf("failed to update %s: %v", s.Name, err)
				} else {
					log.Printf("Successfully updated %s", s.Name)
				}

			default:
				errChan <- fmt.Errorf("path %s exists but is not a directory (skipping)", s.Path)
			}

			// Update progress bar (thread-safe)
			barMutex.Lock()
			bar.Add(1)
			barMutex.Unlock()
		}(sm)
	}

	// Wait for completion and collect errors
	go func() {
		wg.Wait()
		close(errChan)
	}()

	// Print errors
	for err := range errChan {
		log.Println("Error:", err)
	}

	log.Println("Submodule processing complete")
}

// getProxiedURL returns the original URL unless it's a GitHub repo, in which case it adds the proxy
func getProxiedURL(originalURL, proxy string) string {
	// Check if URL is a GitHub repo (supports HTTPS, HTTP, and SSH formats)
	if isGitHubURL(originalURL) {
		// Convert SSH URLs (git@github.com:user/repo.git) to HTTPS first
		httpsURL := convertSSHToHTTPS(originalURL)
		// Trim original protocol to avoid duplicates (e.g., "https://")
		trimmedURL := strings.TrimPrefix(httpsURL, "https://")
		trimmedURL = strings.TrimPrefix(trimmedURL, "http://")
		return fmt.Sprintf("%s%s", proxy, trimmedURL)
	}
	// Not a GitHub URL - return original
	return originalURL
}

// isGitHubURL checks if the URL points to a GitHub repository
func isGitHubURL(url string) bool {
	lowerURL := strings.ToLower(url)
	return strings.Contains(lowerURL, "github.com")
}

// convertSSHToHTTPS converts GitHub SSH URLs (git@github.com:user/repo.git) to HTTPS
func convertSSHToHTTPS(sshURL string) string {
	if strings.HasPrefix(sshURL, "git@github.com:") {
		return "https://github.com/" + strings.TrimPrefix(sshURL, "git@github.com:")
	}
	return sshURL
}

// cloneSubmodule (unchanged)
func cloneSubmodule(targetPath, url string) error {
	parentDir := filepath.Dir(targetPath)
	if err := os.MkdirAll(parentDir, 0755); err != nil {
		return fmt.Errorf("failed to create parent dir: %v", err)
	}

	cmd := exec.Command("git", "clone", url, targetPath)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("clone failed: %v\nOutput: %s", err, string(output))
	}
	return nil
}

// updateSubmodule (unchanged)
func updateSubmodule(targetPath, url string) error {
	gitDir := filepath.Join(targetPath, ".git")
	if _, err := os.Stat(gitDir); os.IsNotExist(err) {
		return fmt.Errorf("path exists but is not a Git repo (no .git directory)")
	}

	cmd := exec.Command("git", "remote", "set-url", "origin", url)
	cmd.Dir = targetPath
	if output, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("failed to set remote URL: %v\nOutput: %s", err, string(output))
	}

	cmd = exec.Command("git", "pull", "origin", "HEAD")
	cmd.Dir = targetPath
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("pull failed: %v\nOutput: %s", err, string(output))
	}
	return nil
}
